/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.response;

import java.io.Writer;
import java.io.IOException;
import java.util.*;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.util.UnicodeUtil;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.DocIterator;
import org.apache.solr.search.DocList;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;

/**
 * A description of the PHP serialization format can be found here:
 * http://www.hurring.com/scott/code/perl/serialize/
 */
public class PHPSerializedResponseWriter implements QueryResponseWriter {
	static String CONTENT_TYPE_PHP_UTF8 = "text/x-php-serialized;charset=UTF-8";

	public void init(NamedList n) {
	}

	public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp)
			throws IOException {
		PHPSerializedWriter w = new PHPSerializedWriter(writer, req, rsp);
		try {
			w.writeResponse();
		} finally {
			w.close();
		}
	}

	public String getContentType(SolrQueryRequest request,
			SolrQueryResponse response) {
		return CONTENT_TYPE_TEXT_UTF8;
	}
}

class PHPSerializedWriter extends JSONWriter {
	final UnicodeUtil.UTF8Result utf8;

	public PHPSerializedWriter(Writer writer, SolrQueryRequest req,
			SolrQueryResponse rsp) {
		super(writer, req, rsp);
		this.utf8 = new UnicodeUtil.UTF8Result();
		// never indent serialized PHP data
		doIndent = false;
	}

	@Override
	public void writeResponse() throws IOException {
		Boolean omitHeader = req.getParams().getBool(CommonParams.OMIT_HEADER);
		if (omitHeader != null && omitHeader)
			rsp.getValues().remove("responseHeader");
		writeNamedList(null, rsp.getValues());
	}

	@Override
	public void writeNamedList(String name, NamedList val) throws IOException {
		writeNamedListAsMapMangled(name, val);
	}

	@Override
	public void writeDoc(String name, Collection<Fieldable> fields,
			Set<String> returnFields, Map pseudoFields) throws IOException {
		ArrayList<Fieldable> single = new ArrayList<Fieldable>();
		LinkedHashMap<String, MultiValueField> multi = new LinkedHashMap<String, MultiValueField>();

		for (Fieldable ff : fields) {
			String fname = ff.name();
			if (returnFields != null && !returnFields.contains(fname)) {
				continue;
			}
			// if the field is multivalued, it may have other values further
			// on... so
			// build up a list for each multi-valued field.
			SchemaField sf = schema.getField(fname);
			if (sf.multiValued()) {
				MultiValueField mf = multi.get(fname);
				if (mf == null) {
					mf = new MultiValueField(sf, ff);
					multi.put(fname, mf);
				} else {
					mf.fields.add(ff);
				}
			} else {
				single.add(ff);
			}
		}

		// obtain number of fields in doc
		writeArrayOpener(single.size() + multi.size()
				+ ((pseudoFields != null) ? pseudoFields.size() : 0));

		// output single value fields
		for (Fieldable ff : single) {
			SchemaField sf = schema.getField(ff.name());
			writeKey(ff.name(), true);
			sf.write(this, ff.name(), ff);
		}

		// output multi value fields
		for (MultiValueField mvf : multi.values()) {
			writeKey(mvf.sfield.getName(), true);
			writeArrayOpener(mvf.fields.size());
			int i = 0;
			for (Fieldable ff : mvf.fields) {
				writeKey(i++, false);
				mvf.sfield.write(this, null, ff);
			}
			writeArrayCloser();
		}

		// output pseudo fields
		if (pseudoFields != null && pseudoFields.size() > 0) {
			writeMap(null, pseudoFields, true, false);
		}
		writeArrayCloser();
	}

	@Override
	public void writeDocList(String name, DocList ids, Set<String> fields,
			Map otherFields) throws IOException {
		boolean includeScore = false;

		if (fields != null) {
			includeScore = fields.contains("score");
			if (fields.size() == 0 || (fields.size() == 1 && includeScore)
					|| fields.contains("*")) {
				fields = null; // null means return all stored fields
			}
		}

		int sz = ids.size();

		writeMapOpener(includeScore ? 4 : 3);
		writeKey("numFound", false);
		writeInt(null, ids.matches());
		writeKey("start", false);
		writeInt(null, ids.offset());

		if (includeScore) {
			writeKey("maxScore", false);
			writeFloat(null, ids.maxScore());
		}
		writeKey("docs", false);
		writeArrayOpener(sz);

		SolrIndexSearcher searcher = req.getSearcher();
		DocIterator iterator = ids.iterator();
		for (int i = 0; i < sz; i++) {
			int id = iterator.nextDoc();
			Document doc = searcher.doc(id, fields);
			writeKey(i, false);
			writeDoc(null, doc, fields,
					(includeScore ? iterator.score() : 0.0f), includeScore);
		}
		writeMapCloser();

		if (otherFields != null) {
			writeMap(null, otherFields, true, false);
		}

		writeMapCloser();
	}

	@Override
	public void writeSolrDocument(String name, SolrDocument doc,
			Set<String> returnFields, Map pseudoFields) throws IOException {
		LinkedHashMap<String, Object> single = new LinkedHashMap<String, Object>();
		LinkedHashMap<String, Object> multi = new LinkedHashMap<String, Object>();
		int pseudoSize = pseudoFields != null ? pseudoFields.size() : 0;

		for (String fname : doc.getFieldNames()) {
			if (returnFields != null && !returnFields.contains(fname)) {
				continue;
			}

			Object val = doc.getFieldValue(fname);
			SchemaField sf = schema.getFieldOrNull(fname);
			if (sf != null && sf.multiValued()) {
				multi.put(fname, val);
			} else {
				single.put(fname, val);
			}
		}

		writeMapOpener(single.size() + multi.size() + pseudoSize);
		for (String fname : single.keySet()) {
			Object val = single.get(fname);
			writeKey(fname, true);
			writeVal(fname, val);
		}

		for (String fname : multi.keySet()) {
			writeKey(fname, true);

			Object val = multi.get(fname);
			if (!(val instanceof Collection)) {
				// should never be reached if multivalued fields are stored as a
				// Collection
				// so I'm assuming a size of 1 just to wrap the single value
				writeArrayOpener(1);
				writeVal(fname, val);
				writeArrayCloser();
			} else {
				writeVal(fname, val);
			}
		}

		if (pseudoSize > 0) {
			writeMap(null, pseudoFields, true, false);
		}
		writeMapCloser();
	}

	@Override
	public void writeSolrDocumentList(String name, SolrDocumentList docs,
			Set<String> fields, Map otherFields) throws IOException {
		boolean includeScore = false;
		if (fields != null) {
			includeScore = fields.contains("score");
			if (fields.size() == 0 || (fields.size() == 1 && includeScore)
					|| fields.contains("*")) {
				fields = null; // null means return all stored fields
			}
		}

		int sz = docs.size();

		writeMapOpener(includeScore ? 4 : 3);

		writeKey("numFound", false);
		writeLong(null, docs.getNumFound());

		writeKey("start", false);
		writeLong(null, docs.getStart());

		if (includeScore && docs.getMaxScore() != null) {
			writeKey("maxScore", false);
			writeFloat(null, docs.getMaxScore());
		}

		writeKey("docs", false);

		writeArrayOpener(sz);
		for (int i = 0; i < sz; i++) {
			writeKey(i, false);
			writeSolrDocument(null, docs.get(i), fields, otherFields);
		}
		writeArrayCloser();

		if (otherFields != null) {
			writeMap(null, otherFields, true, false);
		}
		writeMapCloser();
	}

	@Override
	public void writeArray(String name, Object[] val) throws IOException {
		writeMapOpener(val.length);
		for (int i = 0; i < val.length; i++) {
			writeKey(i, false);
			writeVal(String.valueOf(i), val[i]);
		}
		writeMapCloser();
	}

	@Override
	public void writeArray(String name, Iterator val) throws IOException {
		ArrayList vals = new ArrayList();
		while (val.hasNext()) {
			vals.add(val.next());
		}
		writeArray(name, vals.toArray());
	}

	@Override
	public void writeMapOpener(int size) throws IOException,
			IllegalArgumentException {
		// negative size value indicates that something has gone wrong
		if (size < 0) {
			throw new IllegalArgumentException("Map size must not be negative");
		}
		writer.write("a:" + size + ":{");
	}

	@Override
	public void writeMapSeparator() throws IOException {
		/* NOOP */
	}

	@Override
	public void writeMapCloser() throws IOException {
		writer.write('}');
	}

	@Override
	public void writeArrayOpener(int size) throws IOException,
			IllegalArgumentException {
		// negative size value indicates that something has gone wrong
		if (size < 0) {
			throw new IllegalArgumentException(
					"Array size must not be negative");
		}
		writer.write("a:" + size + ":{");
	}

	@Override
	public void writeArraySeparator() throws IOException {
		/* NOOP */
	}

	@Override
	public void writeArrayCloser() throws IOException {
		writer.write('}');
	}

	@Override
	public void writeNull(String name) throws IOException {
		writer.write("N;");
	}

	@Override
	protected void writeKey(String fname, boolean needsEscaping)
			throws IOException {
		writeStr(null, fname, needsEscaping);
	}

	void writeKey(int val, boolean needsEscaping) throws IOException {
		writeInt(null, String.valueOf(val));
	}

	@Override
	public void writeBool(String name, boolean val) throws IOException {
		writer.write(val ? "b:1;" : "b:0;");
	}

	@Override
	public void writeBool(String name, String val) throws IOException {
		writeBool(name, val.charAt(0) == 't');
	}

	@Override
	public void writeInt(String name, String val) throws IOException {
		writer.write("i:" + val + ";");
	}

	@Override
	public void writeLong(String name, String val) throws IOException {
		writeInt(name, val);
	}

	@Override
	public void writeFloat(String name, String val) throws IOException {
		writeDouble(name, val);
	}

	@Override
	public void writeDouble(String name, String val) throws IOException {
		writer.write("d:" + val + ";");
	}

	@Override
	public void writeStr(String name, String val, boolean needsEscaping)
			throws IOException {
		// serialized PHP strings don't need to be escaped at all, however the
		// string size reported needs be the number of bytes rather than chars.
		UnicodeUtil.UTF16toUTF8(val, 0, val.length(), utf8);
		int nBytes = utf8.length;

		writer.write("s:");
		writer.write(Integer.toString(nBytes));
		writer.write(":\"");
		writer.write(val);
		writer.write("\";");
	}
}
